/*
 * The Computer Language Benchmarks Game
 * https://salsa.debian.org/benchmarksgame-team/benchmarksgame/
 *
 * Based on Java (4) implementation modified by
 * Mehmet D. AKIN and Rikard Mustajärvi
 *
 * Contributed by Hank Grabowski
 * Adapted for Kotlin/Native by [Your Name]
 */

@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)

import kotlinx.cinterop.*
import platform.posix.*
import kotlin.native.concurrent.*

internal object Fasta {
    private const val IM = 139968
    private const val IA = 3877
    private const val IC = 29573

    private const val LINE_LENGTH = 60
    private const val BUFFER_SIZE = (LINE_LENGTH + 1) * 1024 // add 1 for '\n'

    // Weighted selection from alphabet
    private val ALU =
        "GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGG" +
                "GAGGCCGAGGCGGGCGGATCACCTGAGGTCAGGAGTTCGAGA" +
                "CCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAAT" +
                "ACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCA" +
                "GCTACTCGGGAGGCTGAGGCAGGAGAATCGCTTGAACCCGGG" +
                "AGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCC" +
                "AGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAA"

    private class FloatProbFreq(val chars: ByteArray, probs: DoubleArray) {
        val probs: FloatArray

        init {
            this.probs = FloatArray(probs.size)
            for (i in probs.indices) {
                this.probs[i] = probs[i].toFloat()
            }
            makeCumulative()
        }

        private fun makeCumulative() {
            var cp = 0.0
            for (i in probs.indices) {
                cp += probs[i].toDouble()
                probs[i] = cp.toFloat()
            }
        }

        fun selectRandomIntoBuffer(buffer: ByteArray, bufferIndex: Int, nRandom: Int): Int {
            var idx = bufferIndex
            val len = probs.size

            outer@ for (rIndex in 0 until nRandom) {
                val r = random(1.0f)
                for (i in 0 until len) {
                    if (r < probs[i]) {
                        buffer[idx++] = chars[i]
                        continue@outer
                    }
                }
                buffer[idx++] = chars[len - 1]
            }

            return idx
        }

        companion object {
            var last = 42

            // pseudo-random number generator
            fun random(max: Float): Float {
                val oneOverIM = 1.0f / IM
                last = (last * IA + IC) % IM
                return max * last.toFloat() * oneOverIM
            }
        }
    }

    private val IUB = FloatProbFreq(
        byteArrayOf('a'.toByte(), 'c'.toByte(), 'g'.toByte(), 't'.toByte(), 'B'.toByte(), 'D'.toByte(),
            'H'.toByte(), 'K'.toByte(), 'M'.toByte(), 'N'.toByte(), 'R'.toByte(), 'S'.toByte(),
            'V'.toByte(), 'W'.toByte(), 'Y'.toByte()),
        doubleArrayOf(0.27, 0.12, 0.12, 0.27, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02)
    )

    private val HOMO_SAPIENS = FloatProbFreq(
        byteArrayOf('a'.toByte(), 'c'.toByte(), 'g'.toByte(), 't'.toByte()),
        doubleArrayOf(0.3029549426680, 0.1979883004921, 0.1975473066391, 0.3015094502008)
    )

    private fun makeRandomFasta(id: String, desc: String, fpf: FloatProbFreq, nChars: Int) {
        var remainingChars = nChars
        val buffer = ByteArray(BUFFER_SIZE)

        val descStr = ">$id $desc\n"
        descStr.encodeToByteArray().usePinned { pinned ->
            fwrite(pinned.addressOf(0), 1.convert(), descStr.length.convert(), stdout)
        }

        var bufferIndex = 0
        while (remainingChars > 0) {
            val chunkSize = minOf(LINE_LENGTH, remainingChars)

            if (bufferIndex == BUFFER_SIZE) {
                buffer.usePinned { pinned ->
                    fwrite(pinned.addressOf(0), 1.convert(), bufferIndex.convert(), stdout)
                }
                bufferIndex = 0
            }

            bufferIndex = fpf.selectRandomIntoBuffer(buffer, bufferIndex, chunkSize)
            buffer[bufferIndex++] = '\n'.toByte()

            remainingChars -= chunkSize
        }

        if (bufferIndex > 0) {
            buffer.usePinned { pinned ->
                fwrite(pinned.addressOf(0), 1.convert(), bufferIndex.convert(), stdout)
            }
        }
    }

    private fun makeRepeatFasta(id: String, desc: String, alu: String, nChars: Int) {
        var remainingChars = nChars
        val aluBytes = alu.encodeToByteArray()
        var aluIndex = 0
        val buffer = ByteArray(BUFFER_SIZE)

        val descStr = ">$id $desc\n"
        descStr.encodeToByteArray().usePinned { pinned ->
            fwrite(pinned.addressOf(0), 1.convert(), descStr.length.convert(), stdout)
        }

        var bufferIndex = 0
        while (remainingChars > 0) {
            val chunkSize = minOf(LINE_LENGTH, remainingChars)

            if (bufferIndex == BUFFER_SIZE) {
                buffer.usePinned { pinned ->
                    fwrite(pinned.addressOf(0), 1.convert(), bufferIndex.convert(), stdout)
                }
                bufferIndex = 0
            }

            for (i in 0 until chunkSize) {
                if (aluIndex == aluBytes.size) {
                    aluIndex = 0
                }
                buffer[bufferIndex++] = aluBytes[aluIndex++]
            }
            buffer[bufferIndex++] = '\n'.toByte()

            remainingChars -= chunkSize
        }

        if (bufferIndex > 0) {
            buffer.usePinned { pinned ->
                fwrite(pinned.addressOf(0), 1.convert(), bufferIndex.convert(), stdout)
            }
        }
    }

    fun main(args: Array<String>) {
        val n = args.firstOrNull()?.toInt() ?: 1000

        makeRepeatFasta("ONE", "Homo sapiens alu", ALU, n * 2)
        makeRandomFasta("TWO", "IUB ambiguity codes", IUB, n * 3)
        makeRandomFasta("THREE", "Homo sapiens frequency", HOMO_SAPIENS, n * 5)

        fflush(stdout)
    }
}

fun main(args: Array<String>) = memScoped {
    Fasta.main(args)
}